package com.young4j.springdemo.config.redis;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.dao.DataAccessException;
import org.springframework.data.redis.connection.RedisConnection;
import org.springframework.data.redis.core.RedisCallback;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.data.redis.core.script.DefaultRedisScript;
import org.springframework.stereotype.Component;
import redis.clients.jedis.Jedis;

import java.util.Collections;
import java.util.List;

/**
 * @Description
 * @Author yf
 * @Date 2019/1/25 13:40
 **/
@Component
public class RedisAtomicClient {
    private static final Logger logger = LoggerFactory.getLogger(RedisAtomicClient.class);
    private final RedisTemplate redisTemplate;
    private final StringRedisTemplate stringRedisTemplate;
    private static final String INCR_BY_WITH_TIMEOUT = "local v;" +
            " v = redis.call('incrBy',KEYS[1],ARGV[1]);" +
            "if tonumber(v) == 1 then\n" +
            "    redis.call('expire',KEYS[1],ARGV[2])\n" +
            "end\n" +
            "return v";
    private static final String COMPARE_AND_DELETE =
            "if redis.call('get',KEYS[1]) == ARGV[1]\n" +
                    "then\n" +
                    "    return redis.call('del',KEYS[1])\n" +
                    "else\n" +
                    "    return 0\n" +
                    "end";

    public RedisAtomicClient(RedisTemplate redisTemplate) {
        this.redisTemplate = redisTemplate;
        this.stringRedisTemplate = new StringRedisTemplate();
        this.stringRedisTemplate.setConnectionFactory(redisTemplate.getConnectionFactory());
        this.stringRedisTemplate.afterPropertiesSet();
    }

    /**
     * 返回0,1用于确认是否成功获取锁
     * 这里使用redisTemplate执行的脚本，使用stringRedisTemplate不能查询到已添加的值
     * @param key
     * @param value
     * @return
     */
    public Long lock(String key, String value) {
        String script = "local key = KEYS[1]; local value = ARGV[1]; if redis.call('set', key, value, 'NX' ,'PX', 20000) then return 1 else return 0 end";
        DefaultRedisScript<Long> redisScript = new DefaultRedisScript<>(script, Long.class);
        Object execute = redisTemplate.execute(redisScript, Collections.singletonList(key), Collections.singletonList(value));
        //Object execute = redisTemplate.execute(redisScript, Collections.singletonList(key), value);
        logger.info(execute.toString());
        return (Long) execute;
    }

    public Object unlock(String key, String value) {
        String script = "if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del',KEYS[1]) else return 0 end";
        DefaultRedisScript<Long> redisScript = new DefaultRedisScript<>(script, Long.class);
        Long execute = (Long) redisTemplate.execute(redisScript, Collections.singletonList(key), value);
        System.out.println("unlock result: "+execute);
        if(execute != null && execute != 0) {
            // 解锁成功
            return "unlock success";
        } else {
            return "unlock failed";
        }
    }
    /**
     * 获取redis 分布式锁，内部实现使用了setnx
     * 如果锁定失败返回null，获取失败时会自动重试指定的次数,由于多次等待会阻塞当前线程，请尽量避免使用此方法
     * 如果锁定成功返回RedisLock对象，同时调用RedisLock.unlock()方法来释放锁
     * @param key
     * @param expireSeconds
     * @param maxRetryTimes
     * @param retryIntervalTimeMillis 每次重试之前sleep等待的毫秒数
     * @return 获得的锁对象，后续可以调用该对象的unlock方法来释放锁
     */
    public RedisLock getLock(final String key, final long expireSeconds, int maxRetryTimes, long retryIntervalTimeMillis) {
        final String value = key.hashCode() + "";
        int maxTimes = maxRetryTimes + 1;
        for (int i = 0; i < maxTimes; i++) {
            logger.info("第" + i + "次重试！");
            String execute = stringRedisTemplate.execute(new RedisCallback<String>() {
                @Override
                public String doInRedis(RedisConnection redisConnection) throws DataAccessException {
                    Jedis jedis = (Jedis) redisConnection.getNativeConnection();
                    String status = jedis.set(key, value, "nx", "ex", expireSeconds);
                    return status;
                }
            });
            if ("OK".equals(execute)) {
                return new RedisLockInner(stringRedisTemplate, key, value);
            }
            if (retryIntervalTimeMillis > 0) {
                try {
                    Thread.sleep(retryIntervalTimeMillis);
                } catch (InterruptedException e) {
                    break;
                }
            }
            if (Thread.currentThread().isInterrupted()) {
                break;
            }
        }
        return null;
    }
    public class RedisLockInner implements RedisLock{
        private StringRedisTemplate stringRedisTemplate;
        private String key;
        private String expectedValue;

        public RedisLockInner(StringRedisTemplate stringRedisTemplate, String key, String expectedValue) {
            this.stringRedisTemplate = stringRedisTemplate;
            this.key = key;
            this.expectedValue = expectedValue;
        }

        @Override
        public void unlock() {
            List<String> keys = Collections.singletonList(key);
            stringRedisTemplate.execute(new DefaultRedisScript<>(COMPARE_AND_DELETE, String.class), keys, expectedValue);
        }

        @Override
        public void close() throws Exception {
            this.unlock();
        }
    }
}
